[iOS] UIPresentationController でドロワーメニューを作りたい
こんにちは。きんくまです。
ドロワーメニューを作りたくて調べていたら、本ブログで記事が見つかりました!
[iOS 8] UIPresentationController でカスタムのモーダル表示を実装する
[iOS] UIPresentationControllerを使用してカスタムダイアログを実装する
こちらを参考に作ってみました。
作ったもの
GitHubのリポジトリ
cm-tsmaeda/DrawerMenuPresentationControllerSample
1. UIPresentationController のサブクラスを実装
ここでは何を設定するかというと、こんなやつです。
- 新しく表示されるViewのframe(今回はドロワーなので、左に寄せて少し幅を小さくしました)
- 新しく表示されるViewの他に、何かオプション的なViewがあれば表示
- オプション的なViewのアニメーション
今回は、元のViewと新しく表示されるViewの間に、半透明の背景(Webで言うところのざぶとん的な)を追加しました。
実際にどのViewControllerを表示するかは、UIPresentationController自体は知らない(定義していない)ところがポイントですね。なのでいろいろなViewControllerをこのクラスで設定した表示方法で呼び出すことができます。
class DrawerMenuPresentationController: UIPresentationController { // 半透明の背景カバー private var coverBackgroundView: UIView! private var coverBackgroundTapGesture: UITapGestureRecognizer! override func presentationTransitionWillBegin() { super.presentationTransitionWillBegin() coverBackgroundView = UIView() coverBackgroundView.alpha = 0 coverBackgroundView?.backgroundColor = UIColor.black containerView?.insertSubview(coverBackgroundView, at: 0) coverBackgroundTapGesture = UITapGestureRecognizer(target: self, action: #selector(coverBackgroundDidTap)) coverBackgroundView.addGestureRecognizer(coverBackgroundTapGesture) // オプションViewのアニメーション let transitionCoordinator = presentingViewController.transitionCoordinator transitionCoordinator?.animate(alongsideTransition: { [weak self] context in self?.coverBackgroundView.alpha = 0.7 }, completion: nil) } // 半透明の背景を押したら閉じる @objc func coverBackgroundDidTap() { presentingViewController.dismiss(animated: true) { } } override func presentationTransitionDidEnd(_ completed: Bool) { super.presentationTransitionDidEnd(completed) } override func dismissalTransitionWillBegin() { super.dismissalTransitionWillBegin() coverBackgroundView.removeGestureRecognizer(coverBackgroundTapGesture) let transitionCoordinator = presentingViewController.transitionCoordinator transitionCoordinator?.animate(alongsideTransition: { [weak self] context in self?.coverBackgroundView.alpha = 0 }, completion: nil) } override func dismissalTransitionDidEnd(_ completed: Bool) { super.dismissalTransitionDidEnd(completed) if completed { coverBackgroundView.removeFromSuperview() } } override var frameOfPresentedViewInContainerView: CGRect { guard let containerSize = containerView?.frame.size else { return CGRect.zero } let width = containerSize.width * 0.85 return CGRect(x: 0, y: 0, width: width, height: containerSize.height) } override func containerViewWillLayoutSubviews() { super.containerViewWillLayoutSubviews() coverBackgroundView.frame = containerView!.bounds presentedView!.frame = frameOfPresentedViewInContainerView } }
2 UIViewControllerAnimatedTransitioning に適合したクラスを作る
ここで設定することは
- 元のViewと新しく表示されるViewの切り替えアニメーション
になります。今回は、アニメーションを良さげな感じにしたかったので、前回作ったカスタムイージングを使って、UIViewPropertyAnimatorでアニメーションさせています。
表示されるときと、非表示されるときの秒数も切り替えています。ユーザーは非表示するときは速く消したいと思うので、0.1妙短くしています。
あとtransformでアニメーションさせてますが、そこはお好みでframeでやっても良いかと思います。
さらにいうと、setNeedsLayout()をアニメーションの前後で入れています。前に UIViewControllerAnimatedTransitioning を使った時に、表示がおかしくなってハマったことがあったので入れておきました。
ここでもUIPresentationControllerのときと同じく、元のView/ViewController, 新しく表示されるView/ViewController自体は設定されていないので、この切り替えアニメーションを他のところでも使うことが可能です。
class DrawerMenuTransition: NSObject, UIViewControllerAnimatedTransitioning { enum TransitionType { case present case dismiss } var animator: UIViewPropertyAnimator? var transitionType: TransitionType init(type: TransitionType) { self.transitionType = type } func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { switch transitionType { case .present: return 0.4 case .dismiss: return 0.3 } } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { switch transitionType { case .present: startPresentTransition(using: transitionContext) case .dismiss: startDismissTransition(using: transitionContext) } } private func startPresentTransition(using context: UIViewControllerContextTransitioning) { let timing = CubicTimingParametersCreator.createParameters(timingType: .quartOut) let animatorNotNil = UIViewPropertyAnimator(duration: transitionDuration(using: context), timingParameters: timing) let containerView = context.containerView guard let toView = context.view(forKey: .to) else { return } containerView.addSubview(toView) let originalToViewTrans = toView.transform var newToViewTrans = originalToViewTrans newToViewTrans = newToViewTrans.translatedBy(x: -containerView.bounds.width, y: 0) toView.transform = newToViewTrans // ここと! toView.setNeedsLayout() animatorNotNil.addAnimations { toView.transform = originalToViewTrans // ここ! toView.setNeedsLayout() } animatorNotNil.addCompletion { [weak self] _ in self?.animator = nil context.completeTransition(!context.transitionWasCancelled) } animator = animatorNotNil animatorNotNil.startAnimation() } private func startDismissTransition(using context: UIViewControllerContextTransitioning) { let timing = CubicTimingParametersCreator.createParameters(timingType: .quartIn) let animatorNotNil = UIViewPropertyAnimator(duration: transitionDuration(using: context), timingParameters: timing) let containerView = context.containerView guard let fromView = context.view(forKey: .from) else { return } let originalToViewTrans = fromView.transform var newFromViewTrans = originalToViewTrans newFromViewTrans = newFromViewTrans.translatedBy(x: -containerView.bounds.width, y: 0) fromView.setNeedsLayout() animatorNotNil.addAnimations { fromView.transform = newFromViewTrans fromView.setNeedsLayout() } animatorNotNil.addCompletion { [weak self] _ in self?.animator = nil context.completeTransition(!context.transitionWasCancelled) } animator = animatorNotNil animatorNotNil.startAnimation() } }
3 UIViewControllerTransitioningDelegate に適合した、表示される ViewController を設定する
ようやく表示される ViewController を設定します。
今回は、表示されるViewControllerのルートがUINavigationControllerになるのでそこにいろいろと設定しました。
設定する項目としては、手順1と2で作ったものを指定します。
- 自分が UIViewController.present するときに、どの UIPresentationController を使って呼び出されるか
- present / dismiss する時の切り替えアニメーションは、どの UIViewControllerAnimatedTransitioning を使うか
class SettingsNavigationController: UINavigationController { required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) /// 表示のされかたをセット modalPresentationStyle = .custom transitioningDelegate = self } } /// どうやって表示されるか extension SettingsNavigationController: UIViewControllerTransitioningDelegate { func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { return DrawerMenuPresentationController(presentedViewController: presented, presenting: presenting) } func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return DrawerMenuTransition(type: .present) } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return DrawerMenuTransition(type: .dismiss) } }
4 使ってみる!
表示自体は、ふつうに present で可能です。
@IBAction func didTapShowMenuButton() { let storyboad = UIStoryboard(name: "SettingsViewController", bundle: nil) let settingsNavi = storyboad.instantiateInitialViewController() as! UINavigationController // 表示開始はモーダルなので present present(settingsNavi, animated: true, completion: nil) }
感想
クラスごとに役割がきれいに別れているため、別のViewControllerをドロワーメニューとして表示することも簡単にできるのは良いと思いました。
また、UIPresentationControllerを使えば、非同期処理中に見せるXXHUD系のやつとかも、すぐに作れそうですね。
ではでは。